/*
 *
 *  * Copyright 2020 New Relic Corporation. All rights reserved.
 *  * SPDX-License-Identifier: Apache-2.0
 *
 */

package com.newrelic.weave;

import java.util.Collection;

import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.Method;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.FieldInsnNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.MethodNode;

import com.newrelic.weave.utils.WeaveUtils;

/**
 * Represents a method that was generated by the compiler to allow accessing or mutating a new field. We keep track of
 * these so we can rewrite calls to get/set new fields using the extension class.  These methods are generated usually
 * to access a field from a nested or inner class.  This class is primarily used by
 * {@link PreparedExtension#rewriteNewFieldCalls(MethodNode)}.
 *
 * @see PreparedExtension
 */
class GeneratedNewFieldMethod {
    /**
     * Generated method, e.g. access$400
     */
    public final Method method;

    /**
     * Name of the new field the method is operating on.
     */
    public final String newFieldName;

    /**
     * Desc of the new field the method is operating on.
     */
    public final String newFieldDesc;

    /**
     * Opcode representing what the method was doing with the new field, either GETSTATIC, PUTSTATIC, GETFIELD, or
     * PUTFIELD.
     */
    public final int opcode;

    /**
     * All of the generated mutators I've seen so far return the object that was put. When rewriting these to work with
     * the extension class, we will try to keep this behavior.
     * 
     * See {@link PreparedExtension#rewriteNewFieldCalls(MethodNode)} for details on how this is used.
     */
    public final boolean returnsPutValue;

    private GeneratedNewFieldMethod(Method method, String newFieldName, String newFieldDesc, int opcode) {
        this.method = method;
        this.newFieldName = newFieldName;
        this.newFieldDesc = newFieldDesc;
        this.opcode = opcode;
        this.returnsPutValue = (opcode == Opcodes.PUTFIELD || opcode == Opcodes.PUTSTATIC)
                && method.getReturnType() != Type.VOID_TYPE;
    }

    /**
     * Checks the specified method node to see if it was generated with the explicit purpose of field access. Returns a
     * GeneratedNewFieldMethod object representing the operation, or <code>null</code> if the method is for some other
     * purpose other than new field access or mutation.
     * 
     * @param method
     * @param newFields
     * @return
     */
    public static GeneratedNewFieldMethod isGeneratedNewFieldMethod(MethodNode method, Collection<String> newFields) {
        // we assume the method is a new field accessor/mutator iff there is exactly one GET or PUT instruction in the
        // method body and it operates on a new field

        if ((method.access & Opcodes.ACC_SYNTHETIC) == 0 || !WeaveUtils.isSyntheticAccessor(method.name)) {
            return null;
        }

        InsnList instructions = method.instructions;
        for (int i = 1; i < instructions.size() - 1; i++) {
            AbstractInsnNode instruction = instructions.get(i);
            if (instruction.getType() == AbstractInsnNode.FIELD_INSN) {
                // must be a new field
                String fieldName = ((FieldInsnNode) instruction).name;
                if (!newFields.contains(fieldName)) {
                    return null;
                }

                // must return after the field instruction
                int nextInsnOpcode = instructions.get(i + 1).getOpcode();
                if (nextInsnOpcode < Opcodes.IRETURN && nextInsnOpcode > Opcodes.RETURN) {
                    return null;
                }

                return new GeneratedNewFieldMethod(new Method(method.name, method.desc), fieldName,
                        ((FieldInsnNode) instruction).desc, instruction.getOpcode());

            } else if (instruction.getType() == AbstractInsnNode.METHOD_INSN) {
                return null; // there should be no method invocations
            }
        }
        return null;
    }
}
